/*

   **********************************************************************
   *                                                                    *
   * This file contains the code for an image object for a content-     *
   * based image retrieval application that "summarizes" an image and   *
   * sends the resulting image "signature" to a server for              *
   * classification. The server returns the classification result in    *
   * the form of a Web page containing example images of the input      *
   * image's category.                                                  *
   *                                                                    *
   * The image class defined here is rather specific to this particular *
   * application and is not really designed for broad reuse (just yet). *
   *                                                                    *
   **********************************************************************
*/

/*
   **********************************************************************
   *                                                                    *
   * Gist   -- content-based image retrieval application for the gPhone *
   *                                                                    *
   * Copyright (C) 2011 Yong Zeng                                       *
   *                                                                    *
   * This file is part of Gist.                                       *
   *                                                                    *
   * Gist is free software; you can redistribute it and/or modify it  *
   * under the terms of the GNU General Public License as published by  *
   * the Free Software Foundation; either version 2 of the License, or  *
   * (at your option) any later version.                                *
   *                                                                    *
   * Gist is distributed in the hope that it will be useful, but      *
   * WITHOUT ANY WARRANTY; without even the implied warranty of         *
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU   *
   * General Public License for more details.                           *
   *                                                                    *
   * You should have received a copy of the GNU General Public License  *
   * along with Goggle; if not, write to the Free Software Foundation,  *
   * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.       *
   *                                                                    *
   **********************************************************************
*/

//----------------------- PACKAGE SPECIFICATION -------------------------

package vision.gistcomputing ;

//------------------------------ IMPORTS --------------------------------

// Android media support
import android.media.RingtoneManager ;

// Android graphics support
import android.graphics.Bitmap ;

// Android application and OS support
import android.app.Service ;
import android.os.IBinder ;
import android.content.Context ;
import android.content.Intent ;
import android.os.Handler ;
import android.os.Message ;

// Android utilities
import android.util.Log ;

// Java I/O support
import java.io.PrintStream ;
import java.io.FileOutputStream ;
import java.io.File ;
import java.io.IOException ;

// Java thread support
import java.lang.Thread ;

// Java utilities
import java.text.DecimalFormat ;

import vision.gistcomputing.R;

//------------------------- THE TEST SERVICE ----------------------------

/**
   This class implements a background service that excercises Goggle's
   gist vector computation modules and collects various statistics such
   as time taken to compute gist vectors. The gist vectors themselves are
   stored in text files on the phone and must be pulled from it for
   off-line analysis.
*/
public class TestService extends Service {

/// This flag is used to check whether the test service is already up or
/// not.
static boolean m_running = false ;

/// Since computing gist vectors can be quite CPU-intensive, the test
/// service spawns a worker thread to do all the number crunching. The
/// worker thread sends the main thread a message when it is done so that
/// the running status can be updated. These codes serve as the IDs of
/// the messages exchanged between the main and worker threads.
///@{
static final int TESTS_DONE_MSG = 1 ;
///@}

/// This handler object is the one used by the gist computation worker
/// thread to send various notifications to the main thread.
Handler m_status_handler = new Handler() {
   public void handleMessage(Message M) {
      switch (M.what) {
         case TESTS_DONE_MSG:
            wind_up_service() ;
            break ;
      }
   }
} ;

//-------------------------- INITIALIZATION -----------------------------

/// A convenience function for starting the testing service.
public static final void start(Context C)
{
   if (m_running)
      Utils.notify_long(C, C.getString(R.string.test_service_already_up)) ;
   else
      C.startService(new Intent(C, TestService.class)) ;
}

/// This service does not allow clients to connect to it. It just chugs
/// away in the background doing its thang.
public IBinder onBind(Intent I)
{
   return null ;
}

/// This method is called when the service is first created. In the case
/// of the Goggle application, this service will be started by the main
/// screen. When it starts, the service immediately spawns a worker
/// thread to get the job done and then just waits till that thread is
/// done. This ensures that Android doesn't complain about the
/// application's main thread being unresponsive.
@Override public void onCreate()
{
   super.onCreate() ;

   // Spawn worker thread to perform gist computations on test images
   new GistThread(this, m_status_handler) ;
   m_running = true ;
}

//----------------------------- CLEAN-UP --------------------------------

void wind_up_service()
{
   m_running = false ;
   Utils.notify(this, "TestService done") ;
   RingtoneManager.getRingtone(this,
      RingtoneManager.getValidRingtoneUri(this)).play() ;
   stopSelf() ;
}

//------------------ GIST VECTOR COMPUTATION THREAD ---------------------

/**
   Since computing gist vectors is a fairly expensive operation, the test
   service's main thread spawns this worker thread to perform the
   necessary number crunching. This allows the main thread to continue to
   respond to system events, thereby avoiding Android's dreaded
   "Application not responding" messages.
*/
private class GistThread extends Thread {

/// To load the image from the Android MediaStore.Images database and for
/// other such tasks, the worker thread will need a valid Android
/// context.
private Context m_context ;

/// The gist activity's worker thread for computing gist vectors sends
/// messages to the activity's main thread at the start or end of each
/// milestone so that the main thread can update the UI to let users know
/// what's going on. These messages are relayed to the main thread via a
/// suitable Handler object passed to the worker thread by the gist
/// activity.
private Handler m_status_handler ;

/// These are the subset of Caltech-101 categories that we're testing on.
final String[] CATEGORIES = new String[] {
   "car_side",
   "cellphone",
   "chair",
   "cup",
   "faces",
   "laptop",
   "motorbikes",
   "pizza",
} ;

/// These are the different vocabulary sizes used for the tests.
final int[] VOCABULARY_SIZES = new int[] {25, 50, 100, 200} ;

/// Testing requires images and SIFT vocabularies to be stored in the
/// data path directory under the application's files area.
///
/// This directory is assumed to have images named car_side_0001.jpg,
/// car_side_0002.jpg, ..., pizza_0001.jpg, and so on. Additionally, the
/// SIFT vocabularies of different sizes must be present and named
/// sift_vocabulary_25.txt, sift_vocabulary_50.txt, and so on. The
/// testing procedure will load each of these images in turn and compute
/// their gist vectors w.r.t. each vocabulary.
///
/// We want to test classification accuracy in the following two
/// situations: when 25% of the images in each category are used for
/// training and the remaining 75% for testing; and vice versa, i.e., 75%
/// for training and 25% for testing. The way the test service is set up,
/// this means we have two separate runs. In the first run, we push the
/// 75% testing images and corresponding vocabularies. In the second run,
/// we push the 25% testing images and corresponding vocabularies.
///
/// Test results are stored in the results directory under the
/// application's files area. This directory will have subdirectories
/// corresponding to each of the different vocabulary sizes. These
/// subdirectories will contain files named gv_car_side.txt,
/// gv_cellphone.txt, and so on. These files will have the gist vectors
/// for each of the testing images organized by category.
///
/// The entire testing directory hierarchy has to be pushed to the phone
/// prior to testing and then the results pulled from it for off-line
/// classification and analysis.
///@{
String m_data_path ;
String m_results_path ;
///@}

/// In addition to the gist vectors themselves, the test service also
/// records the average time it takes to compute a gist vector for each
/// vocabulary size.
///@{
long[] m_total_times   = new long[VOCABULARY_SIZES.length] ;
int[]  m_sample_counts = new int [VOCABULARY_SIZES.length] ;
///@}

/// When the worker thread object is created, we immediately start it
/// running so that the test service (this class's client) only needs to
/// instantitate it and not have to go through any extensive set-up
/// procedures.
public GistThread(Context C, Handler H)
{
   super("TestService.GistThread") ;

   m_context = C ;
   m_status_handler = H ;

   final String files_path = C.getFilesDir().getPath() + File.separator ;
   m_data_path    = files_path + "data/" ;
   m_results_path = files_path + "results/" ;

   start() ;
}

/// This is the "main" method for the gist vector worker thread. It goes
/// through each of the required steps for computing the gist vectors
/// associated with the testing images and collecting the required
/// statistics.
public void run()
{
   try
   {
      final String files_path = m_data_path ;
      final int N = CATEGORIES.length ;
      for  (int i = 0; i < N; ++i)
      {
         ComputeAndSaveGistVectors compute_and_save_gist_vectors_for_category =
            new ComputeAndSaveGistVectors(CATEGORIES[i]) ;
         Utils.for_each_file(files_path + CATEGORIES[i] + "_[0-9][0-9]*\\.jpg",
                             compute_and_save_gist_vectors_for_category) ;
      }

      PrintStream out =
         new PrintStream(new FileOutputStream(m_results_path + "/times.txt")) ;
      DecimalFormat format = new DecimalFormat("#.#") ;

      final int n = VOCABULARY_SIZES.length ;
      for  (int i = 0; i < n; ++i)
      {
         float average_time = m_total_times[i]/m_sample_counts[i]/1000.0f ;
         out.println(VOCABULARY_SIZES[i] + " word vocabulary: " +
                     format.format(average_time) + " seconds") ;
      }
      out.close() ;
   }
   catch (Exception e)
   {
      Log.e("Goggle", "error in testing service", e) ;
   }

   Utils.notify_main_thread(m_status_handler, TESTS_DONE_MSG) ;
}

//--------------------- GIST VECTOR COMPUTATIONS ------------------------

/// Rather than constantly reallocating a buffer to hold the pixel data
/// for each category's test images, we reuse a common buffer from one
/// iteration of the test loop to the next.
//
// NOTE: This little beastie is part of the the enclosing GistThread
// class and *not* the ComputeAndSaveGistVectors inner class defined in
// this section. This is what allows us to avoid allocating a new buffer
// over and over. If it were part of the inner class, it would have to be
// created anew for each invocation of the ComputeAndSaveGistVectors
// function object.
int[] m_buffer = new int[160 * 120] ;

/// This class implements a function object that computes the gist
/// vectors of all the testing images belonging to a given category and
/// then saves those vectors. Gist vectors are computed for all the
/// vocabulary sizes and percentages of category images used for
/// training/testing.
private class ComputeAndSaveGistVectors implements Utils.Function {

/// Vocabulary files are stored under the test service's data directory
/// with names such as sift_vocabulary_25.txt, sift_vocabulary_50.txt,
/// and so on. This variable "precomputes" the initial "stem" of
/// vocabulary file names to avoid repetitious additions of the same
/// couple of strings in the function object's main loop.
String m_vocabulary_file ;

/// Results are stored in the test service's results directory with names
/// such as gv_car_side.txt, gv_cellphone.txt, and so on. This variable
/// precomputes the final stems of the result file names to avoid having
/// to perform string additions of the same strings over and over inside
/// the function object's main loop.
String m_results_file ;

/// When this function object is instantiated, it should be given the
/// category for which it is to compute and save gist vectors. This is
/// used to construct various file names, etc.
public ComputeAndSaveGistVectors(String category)
{
   m_vocabulary_file = m_data_path + "sift_vocabulary_" ;
   m_results_file = "/gv_" + category + ".txt" ;
}

/// When this function object is applied, it will go through the steps
/// for loading images belonging to the specified category one-by-one,
/// computing their gist vectors for each of the testing vocabularies and
/// then saving those vectors to the proper results files. It also
/// records the total amount of time taken to compute the gist vectors.
public Object apply(Object O)
{
   final String file_name = (String) O ;
   Bitmap B = Utils.load_bitmap(160, 120,
                                new Utils.FileBitmapDecoder(file_name)) ;
   B.getPixels(m_buffer, 0, 160, 0, 0, 160, 120) ;
   B = null ; // free some memory

   try
   {
      Image I = Image.gray_scale(m_buffer, 160, 120) ;
      BbofEstimator GE =
         (BbofEstimator) GistEstimator.create("BbofEstimator") ;

      final int N = VOCABULARY_SIZES.length ;
      for  (int i = 0; i < N; ++i)
      {
         GE.reload_vocabulary(VOCABULARY_SIZES[i], m_vocabulary_file +
                              VOCABULARY_SIZES[i] + ".txt") ;
         long start_time = System.currentTimeMillis() ;
         float[] gv = GE.apply(I) ;
         m_total_times[i] += System.currentTimeMillis() - start_time ;
         ++m_sample_counts[i] ;
         Utils.append(gv, m_results_path + VOCABULARY_SIZES[i] +
                          m_results_file) ;
      }
   }
   catch (GistEstimator.UnknownEstimator e)
   {
      Log.e("Goggle", "Hunh?!? BbofEstimator is unknown?", e) ;
   }

   return null ;
}

} // end of inner class TestService.GistThread.ComputeAndSaveGistVectors
} // end of inner class TestService.GistThread

//-----------------------------------------------------------------------

} 
